using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
using System.IO;
using Newtonsoft.Json;

namespace jMVC.Utils
{
    /// <summary>
    /// The one and only implementation of IPropertyBag
    /// Stores model data and the expected .NET type for each data item
    /// </summary>
    [Serializable]
    internal class PropertyBag : ISerializable, IPropertyBag
    {
        public PropertyBag()
        {
        }

        #region Member variables
        private Dictionary<string, object> m_Data = new Dictionary<string, object>(); // Underlying datastore
        private Dictionary<string, Type> m_ExpectedTypes; // Retrieved during deserialization
        private JsonSerializationHelper m_SerializationHelper = new JsonSerializationHelper();
        public bool SerializeFullData = false; // Determines whether we serialize just a list of keys and types, or this plus the entire set of values
        #endregion

        #region Wrapper functions around the Dictionary
        public object this[string key]
        {
            get { return m_Data[key]; }
            set { m_Data[key] = value; }
        }

        public void Add(string key, object value)
        {
            m_Data.Add(key, value);
        }

        public void Remove(string key)
        {
            m_Data.Remove(key);
        }

        public void Clear()
        {
            m_Data.Clear();
        }

        public IEnumerable<string> Keys
        {
            get { return m_Data.Keys; }
        }

        public bool ContainsKey(string key)
        {
            return m_Data.ContainsKey(key);
        }

        public int Count
        {
            get { return m_Data.Count; }
        }

        #endregion

        #region JSON Serialization
        public void RegisterJsonConverter(JsonConverter converter)
        {
            m_SerializationHelper.RegisterJsonConverter(converter);
        }

        public void WriteSerializedAsJSON(TextWriter writer)
        {
            m_SerializationHelper.WriteSerialized(this.m_Data, writer);
        }

        public string SerializeAsJSON()
        {
            return m_SerializationHelper.Serialize(this.m_Data);
        }

        /// <summary>
        /// Reads the JSON string to populate the PropertyBag, using the list of .NET types
        /// retrieved during deserialization
        /// </summary>
        public void PopulateFromJSON(string json)
        {
            System.IO.StringReader sr = new System.IO.StringReader(json);
            JsonReader reader = new JsonReader(sr);
            reader.Read();

            Clear();
            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonToken.PropertyName:
                        Type expectedType;
                        if(m_ExpectedTypes.ContainsKey((string)reader.Value))
                            expectedType = m_ExpectedTypes[(string)reader.Value];
                        else
                            expectedType = null;
                        this[(string)reader.Value] = m_SerializationHelper.Deserialize(reader, expectedType);
                        break;
                    case JsonToken.EndObject:
                        return;
                    default:
                        throw new JsonSerializationException("Expected property name or end of object - got " + reader.TokenType.ToString());
                }
            }
        }
        #endregion

        #region .NET Serialization
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            // Store Model types so that the JSON deserializer knows what .NET type to deserialize the model into
            // Since it goes into the ViewState, it does expose the type name to anyone who knows about Base64 encoding,
            // but they won't be able to change it (which would be a security issue) because ASP.NET appends and verifies
            // a Machine Authentication Code (MAC) with the viewstate string (by default).
            //
            // Note: for some reason, the .NET Dictionary<string,string> doesn't serialize properly, so
            // am reverting to old-school arrays
            string[] keys = new string[m_Data.Count];
            string[] typeNames = new string[m_Data.Count];
            IEnumerator<string> e = m_Data.Keys.GetEnumerator();
            e.Reset();
            int i = 0;
            while(e.MoveNext()) {
                keys[i] = e.Current;
                typeNames[i] = m_Data[keys[i]] == null ? null : m_Data[keys[i]].GetType().AssemblyQualifiedName;
                i++;
            }
            info.AddValue("keys", keys);
            info.AddValue("typeNames", typeNames);

            // Also, if requested, store entire model data
            info.AddValue("JSONData", SerializeFullData ? this.SerializeAsJSON() : null);
        }

        public PropertyBag(SerializationInfo info, StreamingContext context)
        {
            // Get and parse the list of expected types
            string[] keys = (string[])info.GetValue("keys", typeof(string[]));
            string[] typeNames = (string[])info.GetValue("typeNames", typeof(string[]));
            m_ExpectedTypes = new Dictionary<string, Type>();
            for (int i = 0; i < keys.Length; i++)
                m_ExpectedTypes.Add(keys[i], typeNames[i] == null ? null : Type.GetType(typeNames[i]));

            // If the entire model data is present, read it in
            string json = info.GetString("JSONData");
            if(json != null)
                PopulateFromJSON(json);
        }
        #endregion
    }
}
